﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Threading;
using Nox.Amuse.Net;
using System.Diagnostics;
using Nox.Amuse.Helpers;

namespace Nox.Amuse.Model
{
    /// <summary>
    /// Defines the status of a session's network connection
    /// </summary>
    public enum SessionStatus
    {
        /// <summary>
        /// Currently not connected to a server
        /// </summary>
        Disconnected,
        /// <summary>
        /// Currently looking up a server's address or trying to connect to a server
        /// </summary>
        Connecting,
        /// <summary>
        /// Currently connected to the remote server
        /// </summary>
        Connected
    }

    /// <summary>
    /// Represents a session the user currently has open
    /// </summary>
    public sealed class Session
    {
        /// <summary>
        /// The data source for world information
        /// </summary>
        WorldData WorldData;

        /// <summary>
        /// The TcpClient maintaining the socket
        /// </summary>
        private TcpClient TcpClient;
        /// <summary>
        /// The Stream controlling data flow
        /// </summary>
        private Stream DataStream;
        /// <summary>
        /// The exact Type of the object stored in the DataStream property
        /// </summary>
        private Type StreamType;

        /// <summary>
        /// Stores the dispatcher for returning results from Async Operations
        /// </summary>
        private Dispatcher Dispatcher;

        #region StatusChanged Event
        /// <summary>
        /// Occures when the Status value has changed
        /// This event is thread safe and will fire on the UI thread
        /// </summary>
        public event EventHandler StatusChanged;

        /// <summary>
        /// Invokes the StatusChanged event
        /// </summary>
        private void OnStatusChanged()
        {
            if (Dispatcher.CurrentDispatcher != Dispatcher)
                Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(OnStatusChanged));
            else
            {
                EventHandler safe = StatusChanged;
                if (safe != null)
                    safe(this, new EventArgs());
            }
        }
        #endregion
        /// <summary>
        /// Backing field for Status property
        /// </summary>
        private SessionStatus myStatus;
        /// <summary>
        /// Gets (or privately sets) the status of this session
        /// </summary>
        public SessionStatus Status
        {
            get { return myStatus; }
            private set
            {
                myStatus = value;
                OnStatusChanged();
            }
        }

        /// <summary>
        /// Gets (or privately sets) the last result of a DNS Query
        /// </summary>
        public DnsErrorType LastDnsError { get; private set; }
        /// <summary>
        /// An array of the lastIPAddresses returned by a DNS Query
        /// </summary>
        private IPAddress[] LastReturnedAddresses;
        /// <summary>
        /// Gets (or privately sets) the current IP Address being used by this session
        /// </summary>
        public IPAddress CurrentIPAddress { get; private set; }

        /// <summary>
        /// Gets (or privately sets) the last type of Error that was returned when attempting a connection
        /// </summary>
        public ConnectionErrorTypes LastConnectionError { get; private set; }
        /// <summary>
        /// Gets (or privately sets) a string containing the last error message as received from framework when attempting a connection
        /// </summary>
        public string LastConnectionErrorMessage { get; set; }

        /// <summary>
        /// Stores each line piece of line data received from the game
        /// </summary>
        private Dictionary<Guid, LineItem> Lines;
        /// <summary>
        /// Gets a received Line from the Session based on ID
        /// </summary>
        /// <param name="id">The ID of the line</param>
        /// <returns>The LineItem representing the line</returns>
        public LineItem GetLine(Guid id)
        {
            return Lines[id];
        }
        /// <summary>
        /// The last line to be added to the Lines list
        /// </summary>
        private LineItem PreviousLine;
        /// <summary>
        /// Provides an object for locking access to the Lines Dictionary
        /// </summary>
        private object LineLock = new object();

        /// <summary>
        /// Constructs a new Session with the provided details
        /// </summary>
        /// <param name="worldData">The the data for this session's address and port</param>
        public Session(WorldData worldData)
        {
            Dispatcher = Dispatcher.CurrentDispatcher; //Must be set first to allow thread safe events to use it

            Status = SessionStatus.Disconnected;
            Lines = new Dictionary<Guid, LineItem>();

            WorldData = worldData;
        }

        /// <summary>
        /// Starts a connection attempt in the background
        /// </summary>
        public void StartConnectionAsync()
        {
            Status = SessionStatus.Connecting;

            // Create a little anon-method to call back to

            var connectionAttemptComplete = (Action)delegate
            {
                Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                    (Action)delegate
                    { OnConnectionCompleted(); }
                    );
            };

            try
            {
                bool check = ThreadPool.QueueUserWorkItem(delegate(object o)
                {
                    if (PerformDnsQuery())
                    {
                        foreach (var ip in LastReturnedAddresses)
                        {
                            if (AttemptConnection())
                            {
                                EngageListenLoop();
                                break;
                            }
                            else
                            {
                                CurrentIPAddress = ip;
                            }
                        }
                        if (Status == SessionStatus.Connected)
                        {
                            connectionAttemptComplete.Invoke();
                        }
                        else
                        {
                            Status = SessionStatus.Disconnected;
                            connectionAttemptComplete.Invoke();
                        }
                    }
                    else
                    {
                        Status = SessionStatus.Disconnected;
                        connectionAttemptComplete.Invoke();
                    }
                });
                if (check == false)
                {
                    Status = SessionStatus.Disconnected;
                    connectionAttemptComplete.Invoke();
                }
            }
            catch (ApplicationException appFault)
            {
                System.Diagnostics.Debug.WriteLine("ApplicationException caught: " + appFault.Message);
                LastDnsError = DnsErrorType.CouldNotStart;
                connectionAttemptComplete.Invoke();
            }
            catch (OutOfMemoryException oomFault)
            {
                System.Diagnostics.Debug.WriteLine("OutOfMemoryException caught: " + oomFault.Message);
                LastDnsError = DnsErrorType.CouldNotStart;
                connectionAttemptComplete.Invoke();
            }
        }

        #region ConnectionCompleted Event
        /// <summary>
        /// Indicates the Connection attempt is complete
        /// </summary>
        public event EventHandler ConnectionCompleted;

        /// <summary>
        /// Invokes the ConnectionCompleted event
        /// </summary>
        private void OnConnectionCompleted()
        {
            EventHandler safe = ConnectionCompleted;
            if (safe != null)
                safe(this, new EventArgs());
        }
        #endregion

        #region DnsStageCompleted Event
        /// <summary>
        /// Indicates the Dns Query stage of a connection attempt is complete
        /// </summary>
        public event EventHandler DnsStageCompleted;

        /// <summary>
        /// Invokes the DnsStageCompleted event
        /// </summary>
        private void OnDnsStageCompleted()
        {
            EventHandler safe = DnsStageCompleted;
            if (safe != null)
                safe(this, new EventArgs());
        }
        #endregion

        /// <summary>
        /// Performs a DNS request and sets DNS related properties
        /// </summary>
        /// <remarks>This method will block</remarks>
        /// <returns>True if the query was successful</returns>
        private bool PerformDnsQuery()
        {
            var dnsComplete = (Action)delegate
            {
                Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                    (Action)delegate
                    { OnDnsStageCompleted(); }
                    );
            };

            var result = Connector.QueryDns(WorldData.Address);
            if (result.Error == DnsErrorType.None)
            {
                LastDnsError = DnsErrorType.None;
                LastReturnedAddresses = result.Addresses;
                CurrentIPAddress = LastReturnedAddresses[0];
                dnsComplete.Invoke();
                return true;
            }
            else
            {
                LastDnsError = result.Error;
                dnsComplete.Invoke();
                return false;
            }
        }

        /// <summary>
        /// Performs a connection attempt and starts a connection
        /// </summary>
        /// <remarks>This method will block</remarks>
        /// <returns>True if the attempt was successful</returns>
        private bool AttemptConnection()
        {
            //Reset Telnet Fields
            isProcessingTelnetCommand = false;

            CurrentTelnetCommandStage = TelnetCommandStage.None;
            CurrentTelnetCommandType = TelnetCommandType.SingleCharacter;
            CurrentlyAllowedCommands = new List<AllowedTelnetCommand>();
            CurrentlyActiveCommand = AllowedTelnetCommand.None;

            //TODO: Switch for connection type
            var result = Connector.ConnectToServer(new IPEndPoint(CurrentIPAddress, WorldData.Port), ConnectionOptions.None);
            LastConnectionError = result.Error;
            if (LastConnectionError == ConnectionErrorTypes.None)
            {
                Status = SessionStatus.Connected;
                LastConnectionErrorMessage = string.Empty;
                this.TcpClient = result.Client;
                this.DataStream = result.DataStream;
                this.StreamType = result.StreamType;
                return true;
            }
            else
            {
                LastConnectionErrorMessage = result.ErrorMessage;
                this.TcpClient = null;
                this.DataStream = null;
                this.StreamType = null;
                return false;
            }
        }

        /// <summary>
        /// Begins listening to an established connection for data
        /// </summary>
        private void EngageListenLoop()
        {
            var listenThread = new Thread(new ThreadStart(() =>
            {
                var closeResult = Streamer.PerformListening(DataStream, new DataReceivedHandler(DataReceived));
                Status = SessionStatus.Disconnected;
                Debug.WriteLine("Closed"); //TODO: Post close processing
                if (disconnectExpected)
                    Debug.WriteLine("Expected Disconnection Complete");
                else
                {
                    Debug.WriteLine("Unexpected Disconnection");
                    // Error display here
                }
            }));
            listenThread.IsBackground = true;
            listenThread.Start();
        }

        /// <summary>
        /// If true, the upcoming disconnect was expected
        /// </summary>
        private bool disconnectExpected;

        /// <summary>
        /// Closes the connection to the server
        /// </summary>
        public void Disconnect()
        {
            disconnectExpected = true;
            TcpClient.Client.Close();
        }



        #region LineReceived Event
        /// <summary>
        /// Raised when a complete line of text has been received from the server
        /// </summary>
        public event EventHandler<LineReceivedEventArgs> LineReceived;

        /// <summary>
        /// Invokes the LineReceived event
        /// </summary>
        /// <param name="lineGuid">The Guid for the line that was received</param>
        private void OnLineReceived(Guid lineGuid)
        {
            EventHandler<LineReceivedEventArgs> safe = LineReceived;
            if (safe != null)
                safe(this, new LineReceivedEventArgs(lineGuid));
        }
        #endregion

        /// <summary>
        /// Stores the StringBuilder that will be used for collecting characters in to a single line
        /// </summary>
        StringBuilder LineBuilder = new StringBuilder();

        /// <summary>
        /// If true, we are currently reading a Telnet command from the received data
        /// </summary>
        bool isProcessingTelnetCommand = false;
        /// <summary>
        /// Indicates the current step we are in while processing Telnet Commands
        /// </summary>
        TelnetCommandStage CurrentTelnetCommandStage = TelnetCommandStage.None;
        /// <summary>
        /// Indicates the current TelnetCommandType we are processing
        /// </summary>
        TelnetCommandType CurrentTelnetCommandType = TelnetCommandType.SingleCharacter;
        /// <summary>
        /// Indicates the currently allowed Telnet commands
        /// </summary>
        List<AllowedTelnetCommand> CurrentlyAllowedCommands = new List<AllowedTelnetCommand>();
        /// <summary>
        /// Indicates the Telnet command currently having its suboption Send command being validated
        /// </summary>
        AllowedTelnetCommand CurrentlyActiveCommand = AllowedTelnetCommand.None;
        /// <summary>
        /// Indicates the Telnet suboption currently being read is Invalid
        /// </summary>
        bool isInvalidSubOption = false;

        /// <summary>
        /// Processes a buffer of bytes and converts them in to characters
        /// Additionally checks for new lines and telnet command characters
        /// </summary>
        /// <param name="numberOfBytesRecieved">The number bytes that were collected</param>
        /// <param name="buffer">The collection of characters as byte values</param>
        private void DataReceived(int numberOfBytesRecieved, byte[] buffer)
        {
            for (int i = 0; i < numberOfBytesRecieved; i++)
            {
                // If we're processing a Telnet command, normal processing does not apply
                if (isProcessingTelnetCommand)
                {
                    PerformTelnetProcessing(buffer[i]);
                    continue;
                }

                // If the character is within the printable ASCII character range, add it to the StringBuilder
                if (
                    (buffer[i] >= 32 && buffer[i] < 127) || // Standard characters
                    buffer[i] >= 128 && buffer[i] < 255 || // Extended characters
                    buffer[i] == 27 || // Escape character for ASCII formatting
                    buffer[i] == 9 // Tab character
                    )
                {
                    LineBuilder.Append(Convert.ToChar(buffer[i]));
                    continue;
                }
                else
                {
                    switch (buffer[i])
                    {
                        case 13:
                            // Line Break detected (/r)
                            System.Diagnostics.Debug.WriteLine(LineBuilder.ToString());
                            var line = new LineItem(LineBuilder.ToString(), PreviousLine);
                            lock (LineLock)
                            {
                                Lines.Add(line.Guid, line);
                                PreviousLine = line;
                            }
                            Dispatcher.BeginInvoke(DispatcherPriority.Input, new Action(delegate()
                            {
                                OnLineReceived(line.Guid);
                            }));
                            LineBuilder.Length = 0;
                            break;
                        case 255:
                            // IAC detected
                            isProcessingTelnetCommand = true;
                            CurrentTelnetCommandStage = TelnetCommandStage.CommandType;
                            break;
                        default:
                            // All other characters are ignored
                            break;
                    }
                }
            }
        }

        /// <summary>
        /// Parses a character and performs Telnet functions based upon it and the current Session state
        /// </summary>
        /// <param name="character">The character to parse</param>
        private void PerformTelnetProcessing(byte character)
        {
            var result = TelnetOps.ProcessTelnetControlCharacter(CurrentTelnetCommandStage, CurrentlyAllowedCommands, character);
            // Update stage information
            CurrentTelnetCommandStage = result.TelnetCommandStage;
            // If we have just read the command type, store it
            if ((result.TelnetCommandStage == (result.TelnetCommandStage | TelnetCommandStage.OptionSpec))
                | (result.TelnetCommandStage == (result.TelnetCommandStage | TelnetCommandStage.SubOptionSpec)))
                CurrentTelnetCommandType = result.TelnetCommandType;

            // If IsInvalidSubOption is true, we are at some point in the suboption reading stage but validation has failed
            if (result.IsInvalidSubOption == true)
            {
                CurrentlyActiveCommand = AllowedTelnetCommand.None;
                isInvalidSubOption = true;
            }
            else
            {
                // If IsInvalidSubOption is false and we have just read what the suboption code is, we need to store it
                if (result.TelnetCommandStage == TelnetCommandStage.ValidationStage1)
                {
                    CurrentlyActiveCommand = result.TelnetAllowedCommands;
                    result.TelnetCommandStage = TelnetCommandStage.None;
                }
            }


            // If the returned stage at this point is None,
            // we have just completed the OptionSpec stage, EndSubOption stage, or SubOption validation and need to see what the command was
            if (result.TelnetCommandStage == TelnetCommandStage.None)
            {
                isProcessingTelnetCommand = false; // We are done processing the Telnet command

                // If the returned command type was a single character command, we should process that command
                if (CurrentTelnetCommandType == TelnetCommandType.SingleCharacter)
                {
                    System.Diagnostics.Debug.WriteLine("GA");
                    //System.Windows.MessageBox.Show("GoAhead"); // Call code in Session to do work based on command, then break the loop
                    return;
                }

                // If this was just the EndSubOption, we're done
                if (result.TelnetCommandType == TelnetCommandType.EndSubOption)
                    return;

                //If we have just finished validating a SubOption, we should do the following:
                if (CurrentTelnetCommandType == TelnetCommandType.SubOption)
                {
                    //If the command was not invalidated, send reply back to the server
                    if (!isInvalidSubOption)
                    {
                        switch (CurrentlyActiveCommand)
                        {
                            case AllowedTelnetCommand.Status:
                                //TODO: Err... I probably have to do something here
                                Debug.WriteLine("Status");
                                break;
                            case AllowedTelnetCommand.TerminalType:
                                Debug.WriteLine("TerminalType");
                                SendTerminalType();
                                break;
                            default:
                                throw new InvalidOperationException("An Invalid State occured when sending a sub option reply. Please send a bug report.");
                        }
                    }
                    CurrentlyActiveCommand = AllowedTelnetCommand.None;
                    return;
                }

                //If we have just finished the OptionSpec stage and..
                //If the command type is Do or Will, do the following:
                if (CurrentTelnetCommandType == (CurrentTelnetCommandType | TelnetCommandType.Do)
                    || CurrentTelnetCommandType == (CurrentTelnetCommandType | TelnetCommandType.Will))
                {
                    //If the command is not an invalid command, add it to the list of currently open commands
                    if (result.TelnetAllowedCommands != AllowedTelnetCommand.None)
                    {
                        CurrentlyAllowedCommands.Add(result.TelnetAllowedCommands);
                        Debug.WriteLine("Will " + character);
                        Streamer.SendBytes(DataStream, new byte[] { 255, (byte)TelnetCommandType.Will, character });
                    }
                    else
                    {
                        Debug.WriteLine("Requested but Wont " + character);
                        //If its not a valid command, call code to send a Won't back to the server while we have the command value in buffer[i]
                        Streamer.SendBytes(DataStream, new byte[] { 255, (byte)TelnetCommandType.WillNot, character });
                    }
                }
                else
                {
                    Debug.WriteLine("Asked not to and Wont " + character);
                    //If the command type is Don't or Won't, do the following
                    Streamer.SendBytes(DataStream, new byte[] { 255, (byte)TelnetCommandType.WillNot, character });
                }
            }
        }

        /// <summary>
        /// Sends TerminalType Information to the server
        /// </summary>
        private void SendTerminalType()
        {
            if (Status == SessionStatus.Connected)
            {
                if (!Streamer.SendBytes(DataStream, TelnetOps.GenerateTerminalTypeReply()))
                    Status = SessionStatus.Disconnected;
            }
        }

        /// <summary>
        /// Sends a user typed command to the server
        /// </summary>
        /// <param name="command">The user entered string to send</param>
        public void SendCommand(string command)
        {
            if (Status == SessionStatus.Connected)
            {
                if (!Streamer.SendText(DataStream, command))
                    Status = SessionStatus.Disconnected;
            }
        }
    }

    /// <summary>
    /// Event Arguments for a LineReceivedEvent
    /// </summary>
    public class LineReceivedEventArgs : EventArgs
    {
        /// <summary>
        /// The Guid for the line that was received
        /// </summary>
        public Guid LineGuid { get; private set; }

        /// <summary>
        /// Public constructor for the event arguments
        /// </summary>
        /// <param name="lineGuid">The Guid for the line that was received</param>
        public LineReceivedEventArgs(Guid lineId)
        {
            LineGuid = lineId;
        }
    }
}
